import yfinance as yf
import pandas as pd
import numpy as np
from scipy.optimize import minimize
= 0.03 _RISK_FREE_RATE
# Download AAPL stock data and options data
= "AAPL"
ticker = yf.Ticker(ticker)
stock
# Define the expiration date for the options (example: closest expiry date)
= stock.options[0]
expiry_date
# Get the options chain for the given expiry date
= stock.option_chain(expiry_date)
options_chain
# Filter out deep out-of-the-money (OTM) options
# For example, if AAPL is trading at $150, consider calls with strike > $180 and puts with strike < $120
= stock.history(period="1d")["Close"].iloc[-1]
spot_price = options_chain.calls[options_chain.calls["strike"] > spot_price * 1.2]
deep_otm_calls = options_chain.puts[options_chain.puts["strike"] < spot_price * 0.8]
deep_otm_puts
# Combine and preview the data
= pd.concat([deep_otm_calls, deep_otm_puts])
deep_otm_options "expiry"] = expiry_date deep_otm_options[
def simulate_merton_jump(
float,
init_price: int,
expiry: float,
mu: float,
sigma: float,
lambda_jump: float,
jump_mean: float,
jump_vol: int = 100,
num_paths: int = 252,
num_steps: -> np.ndarray:
) """
Simulate Merton jump diffusion paths using Monte Carlo simulation.
For instance, 30 days (physical date) rather than trading days to expiry, the T = 30/365.
Args:
S0: initial stock price.
T: time to maturity.
mu: drift term.
sigma: diffusion term.
lambda_jump: jump intensity, the average number of jumps per year.
jump_mean: mean of jump size distribution.
jump_std: standard deviation of jump size distribution.
num_paths: number of simulated paths.
num_steps: number of time steps, default to number of trading days in a year.
"""
= expiry / num_steps
dt = np.zeros(shape=(num_paths, num_steps + 1))
paths 0] = init_price # Initial stock price for all paths
paths[:,
for t in range(1, num_steps + 1):
= np.random.normal(loc=0, scale=1, size=num_paths) # Standard Wiener process
W = paths[:, t - 1] * np.exp(
paths[:, t] - 0.5 * sigma**2) * dt + sigma * np.sqrt(dt) * W
(mu
)= np.random.poisson(lam=lambda_jump * dt, size=num_paths)
num_jumps = np.exp(jump_mean + jump_vol * np.random.normal(0, 1, num_paths))
jump_sizes *= np.where(num_jumps > 0, jump_sizes, 1)
paths[:, t]
return paths
def merton_pricing(
*,
float,
S0: float,
K: int,
T: float,
r: float,
mu: float,
sigma: float,
lambda_jump: float,
jump_mean: float,
jump_vol:
num_paths,str,
option_type: -> float:
) """
Price European options under Merton jump diffusion model using simulated path.
First generate simulated paths using the Merton jump diffusion model,
then calculate the option price using the risk-neutral pricing formula.
Args:
S0: initial stock price.
K: strike price.
T: time to maturity.
r: risk-free rate.
mu: drift term.
sigma: diffusion term.
lambda_jump: jump intensity, the average number of jumps per year.
jump_mean: mean of jump size distribution.
jump_std: standard deviation of jump size distribution.
num_paths: number of simulated paths.
option_type: "call" or "put".
Returns:
BS option price.
"""
= simulate_merton_jump(
paths =S0,
init_price=T,
expiry=mu,
mu=sigma,
sigma=lambda_jump,
lambda_jump=jump_mean,
jump_mean=jump_vol,
jump_vol=num_paths,
num_paths
)
= paths[:, -1] # Extract terminal prices
terminal_prices
if option_type == "call":
= np.maximum(terminal_prices - K, 0)
payoffs else:
= np.maximum(K - terminal_prices, 0)
payoffs
return np.exp(-r * T) * np.mean(payoffs) # risk neutral pricing
def objective_func(params: list, market_data, init_value, risk_free_rate) -> float:
"""
Objective function to minimize the sum of squared
errors between market prices and model prices.
Args:
params: model parameters, [mu, sigma, lambda_jump, jump_mean, jump_std].
market_data: option chain data.
init_value: initial stock price.
risk_free_rate: risk-free rate.
Returns:
Sum of squared errors.
"""
= params
mu, sigma, lambda_jump, jump_mean, jump_std = 0
error for _, row in market_data.iterrows():
= row["strike"]
strike = (pd.to_datetime(row["expiry"]) - pd.Timestamp.now()).days / 365
expiry = (row["bid"] + row["ask"]) / 2
market_price = merton_pricing(
model_price =init_value,
S0=strike,
K=expiry,
T=risk_free_rate,
r=mu,
mu=sigma,
sigma=lambda_jump,
lambda_jump=jump_mean,
jump_mean=jump_std,
jump_vol=100,
num_paths="call",
option_type
)+= (market_price - model_price) ** 2
error return error
# Initial guesses for parameters
= [
initial_params 0.05, # mu
0.2, # sigma
0.1, # lambda_jump
-0.1, # jump_mean
0.1, # jump_std
]
# Calibrate using deep OTM options
= minimize(
result
objective_func,
initial_params,=(deep_otm_options, spot_price, _RISK_FREE_RATE),
args="Nelder-Mead",
method
)= result.x
calibrated_params print("Calibrated parameters:", calibrated_params)
Calibrated parameters: [ 0.05200766 0.20268906 0.100778 -0.10256474 0.09463834]